package org.bgee.psp4jdbc.jdbcapi;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.ParameterMetaData;
import java.sql.Ref;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bgee.psp4jdbc.pool.Pool;
import org.bgee.psp4jdbc.pool.PoolElement;
import org.bgee.psp4jdbc.pool.ReleaseException;

/**
 * The aim of this class is to wrap a real <code>java.sql.PreparedStatement</code>, 
 * and to be kept in a <code>PreparedStatement</code> <code>Pool</code> 
 * when its <code>close</code> method is called, instead of being really close.
 * Every time the <code>PreparedStatement</code> goes into the pool, 
 * its parameters are cleared.
 * <p>
 * It will stay in the pool until its parent {@link Connection} brings it back to
 * reuse it.
 * 
 * @author Mathieu Seppey
 * @author Frederic Bastian
 * @version 1.1
 * @since 1
 *
 */
public class PreparedStatement extends PoolElement implements java.sql.PreparedStatement
{

    /**
     * <code>Logger</code> of the class. 
     */
    private final static Logger log = LogManager.getLogger(
            PreparedStatement.class.getName());

    /**
     * The <code>java.sql.Connection</code> that was used 
     * to obtain this <code>PreparedStatement</code>.
     */
    protected final java.sql.Connection connection;
    
    /**
     * The real <code>java.sql.PreparedStatement</code> that this class wraps.
     */
    protected final java.sql.PreparedStatement realPreparedStatement;
    /**
     * A <code>Set</code> to store all psp4jdbc {@code ResultSet}s provided by this 
     * {@code PreparedStatement}, and not yet closed. This will allow to close them if 
     * this {@code PreparedStatement} is closed, before returning to the pool.
     */
    private final Set<ResultSet> resultSets;

    /**
     * Default constructor, should not be used. 
     * Constructor protected, so that only a {@link Connection} can provide 
     * a <code>PreparedStatement</code>.
     * @throws SQLException 
     */
    // Constructor not public, so that only a Connection can provide a PreparedStatement.
    // suppress warnings
    @SuppressWarnings("unused")
    private PreparedStatement() throws SQLException 
    {
        this(null, null, null,null);
    }
    
    /**
     * Constructor used to provide the real <code>java.sql.PreparedStatement</code> 
     * that this class wraps, the <code>Connection</code> used to obtain 
     * this <code>PreparedStatement</code>, and the <code>Pool</code> related to it.
     *  
     * @param id                        A <code>String</code> that represent the unique id
     *                                  of the <code>PreparedStatement</code>.
     * @param pool 						The <code>Pool</code> holding this 
     * 									<code>PreparedStatement</code>.
     * @param connection                The <code>java.sql.Connection</code> that was used 
     *                                  to obtain this <code>PreparedStatement</code>.
     * @param realPreparedStatement     The <code>java.sql.PreparedStatement</code> 
     *                                  that this class wraps
     *                                                         
     */
    public PreparedStatement(String id, Pool pool, java.sql.Connection connection,
            java.sql.PreparedStatement realPreparedStatement)
    {
        super(id, pool);
        
        log.entry(id,connection,realPreparedStatement);
        
        this.connection = connection;
        this.realPreparedStatement = realPreparedStatement;
        this.resultSets = new HashSet<ResultSet>();
        
        log.exit();
    }       

    /**
     * Calls {@code clearParameters} on the real underlying 
     * {@code java.sql.PreparedStatement}, close all {@code ResultSet}s held 
     * by this {@code PreparedStatement}, and puts it back in the pool.
     * 
     * @throws SQLException if the maximum capacity was reached and an error occurred 
     * 						when evicting an element from the pool; or an error occurred 
     *                      when closing the {@code ResultSet}s; or when clearing 
     *                      the parameters.
     */
    @Override
    public void close() throws SQLException {
        log.entry();
        
        this.clearParameters();
        //get a shallow copy of resultSets, because closing the ResultSet 
        //will modify the collection
        Set<ResultSet> shallowCopy = new HashSet<ResultSet>(this.resultSets);
        for (ResultSet rs: shallowCopy) {
            //this method will also remove the ResultSet from resultSets
            rs.close();
        }
        try {
			this.returnToPool();
		} catch (ReleaseException e) {
			throw log.throwing(new SQLException(e));
		}
        
        log.exit();
    }  
    
    /**
     * Call <code>close</code> on the real underlying <code>PreparedStatement</code> 
     * that this object wraps. 
     */
    @Override
    protected void releaseImpl() throws ReleaseException {
    	try {
    		if (!this.realPreparedStatement.isClosed()) {
			    this.realPreparedStatement.close();
    		}
		} catch (SQLException e) {
			throw log.throwing(new ReleaseException(e));
		}
    }
    
    @Override
    public java.sql.Connection getConnection() 
    {
        return this.connection;
    }
    
    /**
     * @return  The real {@code java.sql.PreparedStatement} that this object wraps.
     */
    public java.sql.PreparedStatement getRealStatement() {
    	return this.realPreparedStatement;
    }

    @Override
    public java.sql.ResultSet executeQuery() throws SQLException {
        log.entry();
    	java.sql.ResultSet realResultSet = realPreparedStatement.executeQuery();
    	if (realResultSet == null) {
    		return null;
    	}
        ResultSet rs = new ResultSet(this, realResultSet);
        this.addResultSet(rs);
        return log.exit(rs);
    }

    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
    	java.sql.ResultSet realResultSet = realPreparedStatement.executeQuery(sql);
        if (realResultSet == null) {
    		return null;
    	}
        ResultSet rs = new ResultSet(this, realResultSet);
        this.addResultSet(rs);
        return log.exit(rs);
    }
    
    @Override
    public ResultSet getResultSet() throws SQLException {
    	java.sql.ResultSet realResultSet = realPreparedStatement.getResultSet();
        if (realResultSet == null) {
    		return null;
    	}
        ResultSet rs = new ResultSet(this, realResultSet);
        this.addResultSet(rs);
        return log.exit(rs);
    }

    @Override
    public ResultSet getGeneratedKeys() throws SQLException
    {
    	java.sql.ResultSet realResultSet = realPreparedStatement.getGeneratedKeys();
        if (realResultSet == null) {
    		return null;
    	}
        ResultSet rs = new ResultSet(this, realResultSet);
        this.addResultSet(rs);
        return log.exit(rs);
    }
    
    /**
     * @param rs    A {@code ResultSet} to be added to {@link #resultSets}.
     */
    private void addResultSet(ResultSet rs) {
        log.entry(rs);
        this.resultSets.add(rs);
        log.exit();
    }
    
    /**
     * Notification that a psp4jdbc {@code ResultSet}, held by this 
     * {@code PreparedStatement}, has been closed. Re
     * 
     * @param rs  The {@code ResultSet} that was closed. 
     */
    void resultSetClosed(ResultSet rs) {
        log.entry(rs);
        log.debug("Releasing ResultSet {}", rs);
        this.resultSets.remove(rs);
        log.exit();
    }

    @Override
    public void cancel() throws SQLException {
        realPreparedStatement.cancel();
        this.close();
    }

    //***********************************
    // Methods that are simply delegated.
    //***********************************
      

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException
    {
        return realPreparedStatement.unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException
    {
        return realPreparedStatement.isWrapperFor(iface);
    }

    @Override
    public int executeUpdate(String sql) throws SQLException
    {
        return realPreparedStatement.executeUpdate(sql);
    }

    @Override
    public int executeUpdate() throws SQLException
    {
        return realPreparedStatement.executeUpdate();
    }

    @Override
    public void setNull(int parameterIndex, int sqlType) throws SQLException
    {
        realPreparedStatement.setNull(parameterIndex, sqlType);
    }

    @Override
    public int getMaxFieldSize() throws SQLException
    {
        return realPreparedStatement.getMaxFieldSize();
    }

    @Override
    public void setBoolean(int parameterIndex, boolean x) throws SQLException
    {
        realPreparedStatement.setBoolean(parameterIndex, x);
    }

    @Override
    public void setByte(int parameterIndex, byte x) throws SQLException
    {
        realPreparedStatement.setByte(parameterIndex, x);
    }

    @Override
    public void setMaxFieldSize(int max) throws SQLException
    {
        realPreparedStatement.setMaxFieldSize(max);
    }

    @Override
    public void setShort(int parameterIndex, short x) throws SQLException
    {
        realPreparedStatement.setShort(parameterIndex, x);
    }

    @Override
    public int getMaxRows() throws SQLException
    {
        return realPreparedStatement.getMaxRows();
    }

    @Override
    public void setInt(int parameterIndex, int x) throws SQLException
    {
        realPreparedStatement.setInt(parameterIndex, x);
    }

    @Override
    public void setMaxRows(int max) throws SQLException
    {
        realPreparedStatement.setMaxRows(max);
    }

    @Override
    public void setLong(int parameterIndex, long x) throws SQLException
    {
        realPreparedStatement.setLong(parameterIndex, x);
    }

    @Override
    public void setEscapeProcessing(boolean enable) throws SQLException
    {
        realPreparedStatement.setEscapeProcessing(enable);
    }

    @Override
    public void setFloat(int parameterIndex, float x) throws SQLException
    {
        realPreparedStatement.setFloat(parameterIndex, x);
    }

    @Override
    public void setDouble(int parameterIndex, double x) throws SQLException
    {
        realPreparedStatement.setDouble(parameterIndex, x);
    }

    @Override
    public int getQueryTimeout() throws SQLException
    {
        return realPreparedStatement.getQueryTimeout();
    }

    @Override
    public void setQueryTimeout(int seconds) throws SQLException
    {
        realPreparedStatement.setQueryTimeout(seconds);
    }

    @Override
    public void setBigDecimal(int parameterIndex, BigDecimal x)
            throws SQLException
            {
        realPreparedStatement.setBigDecimal(parameterIndex, x);
            }

    @Override
    public void setString(int parameterIndex, String x) throws SQLException
    {
        realPreparedStatement.setString(parameterIndex, x);
    }

    @Override
    public void setBytes(int parameterIndex, byte[] x) throws SQLException
    {
        realPreparedStatement.setBytes(parameterIndex, x);
    }

    @Override
    public SQLWarning getWarnings() throws SQLException
    {
        return realPreparedStatement.getWarnings();
    }

    @Override
    public void setDate(int parameterIndex, Date x) throws SQLException
    {
        realPreparedStatement.setDate(parameterIndex, x);
    }

    @Override
    public void setTime(int parameterIndex, Time x) throws SQLException
    {
        realPreparedStatement.setTime(parameterIndex, x);
    }

    @Override
    public void clearWarnings() throws SQLException
    {
        realPreparedStatement.clearWarnings();
    }

    @Override
    public void setCursorName(String name) throws SQLException
    {
        realPreparedStatement.setCursorName(name);
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x)
            throws SQLException
            {
        realPreparedStatement.setTimestamp(parameterIndex, x);
            }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, int length)
            throws SQLException
            {
        realPreparedStatement.setAsciiStream(parameterIndex, x, length);
            }

    @Override
    public boolean execute(String sql) throws SQLException
    {
        return realPreparedStatement.execute(sql);
    }

    @Override
    @Deprecated
    public void setUnicodeStream(int parameterIndex, InputStream x, int length)
            throws SQLException
            {
        realPreparedStatement.setUnicodeStream(parameterIndex, x, length);
            }


    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, int length)
            throws SQLException
            {
        realPreparedStatement.setBinaryStream(parameterIndex, x, length);
            }

    @Override
    public int getUpdateCount() throws SQLException
    {
        return realPreparedStatement.getUpdateCount();
    }

    @Override
    public boolean getMoreResults() throws SQLException
    {
        return realPreparedStatement.getMoreResults();
    }

    @Override
    public void clearParameters() throws SQLException
    {
        realPreparedStatement.clearParameters();
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType)
            throws SQLException
            {
        realPreparedStatement.setObject(parameterIndex, x, targetSqlType);
            }

    @Override
    public void setFetchDirection(int direction) throws SQLException
    {
        realPreparedStatement.setFetchDirection(direction);
    }

    @Override
    public int getFetchDirection() throws SQLException
    {
        return realPreparedStatement.getFetchDirection();
    }

    @Override
    public void setObject(int parameterIndex, Object x) throws SQLException
    {
        realPreparedStatement.setObject(parameterIndex, x);
    }

    @Override
    public void setFetchSize(int rows) throws SQLException
    {
        realPreparedStatement.setFetchSize(rows);
    }

    @Override
    public int getFetchSize() throws SQLException
    {
        return realPreparedStatement.getFetchSize();
    }

    @Override
    public int getResultSetConcurrency() throws SQLException
    {
        return realPreparedStatement.getResultSetConcurrency();
    }
    
    @Override
    public boolean execute() throws SQLException
    {
        return realPreparedStatement.execute();
    }
    
    @Override
    public int getResultSetType() throws SQLException
    {
        return realPreparedStatement.getResultSetType();
    }
    
    @Override
    public void addBatch(String sql) throws SQLException
    {
        realPreparedStatement.addBatch(sql);
    }
    
    @Override
    public void clearBatch() throws SQLException
    {
        realPreparedStatement.clearBatch();
    }
    
    @Override
    public void addBatch() throws SQLException
    {
        realPreparedStatement.addBatch();
    }
    
    @Override
    public int[] executeBatch() throws SQLException
    {
        return realPreparedStatement.executeBatch();
    }
    
    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, int length)
            throws SQLException
            {
        realPreparedStatement
        .setCharacterStream(parameterIndex, reader, length);
            }
    
    @Override
    public void setRef(int parameterIndex, Ref x) throws SQLException
    {
        realPreparedStatement.setRef(parameterIndex, x);
    }
    
    @Override
    public void setBlob(int parameterIndex, Blob x) throws SQLException
    {
        realPreparedStatement.setBlob(parameterIndex, x);
    }
    
    @Override
    public void setClob(int parameterIndex, Clob x) throws SQLException
    {
        realPreparedStatement.setClob(parameterIndex, x);
    }
    
    @Override
    public void setArray(int parameterIndex, Array x) throws SQLException
    {
        realPreparedStatement.setArray(parameterIndex, x);
    }
    
    @Override
    public ResultSetMetaData getMetaData() throws SQLException
    {
        return realPreparedStatement.getMetaData();
    }
    
    @Override
    public boolean getMoreResults(int current) throws SQLException
    {
        return realPreparedStatement.getMoreResults(current);
    }
    
    @Override
    public void setDate(int parameterIndex, Date x, Calendar cal)
            throws SQLException
            {
        realPreparedStatement.setDate(parameterIndex, x, cal);
            }
    
    @Override
    public void setTime(int parameterIndex, Time x, Calendar cal)
            throws SQLException
            {
        realPreparedStatement.setTime(parameterIndex, x, cal);
            }
    
    @Override
    public int executeUpdate(String sql, int autoGeneratedKeys)
            throws SQLException
            {
        return realPreparedStatement.executeUpdate(sql, autoGeneratedKeys);
            }
    
    @Override
    public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal)
            throws SQLException
            {
        realPreparedStatement.setTimestamp(parameterIndex, x, cal);
            }
    
    @Override
    public void setNull(int parameterIndex, int sqlType, String typeName)
            throws SQLException
            {
        realPreparedStatement.setNull(parameterIndex, sqlType, typeName);
            }
    
    @Override
    public int executeUpdate(String sql, int[] columnIndexes)
            throws SQLException
            {
        return realPreparedStatement.executeUpdate(sql, columnIndexes);
            }
    
    @Override
    public void setURL(int parameterIndex, URL x) throws SQLException
    {
        realPreparedStatement.setURL(parameterIndex, x);
    }
    
    @Override
    public int executeUpdate(String sql, String[] columnNames)
            throws SQLException
            {
        return realPreparedStatement.executeUpdate(sql, columnNames);
            }
    
    @Override
    public ParameterMetaData getParameterMetaData() throws SQLException
    {
        return realPreparedStatement.getParameterMetaData();
    }
    
    @Override
    public void setRowId(int parameterIndex, RowId x) throws SQLException
    {
        realPreparedStatement.setRowId(parameterIndex, x);
    }
    
    @Override
    public void setNString(int parameterIndex, String value)
            throws SQLException
            {
        realPreparedStatement.setNString(parameterIndex, value);
            }
    
    @Override
    public boolean execute(String sql, int autoGeneratedKeys)
            throws SQLException
            {
        return realPreparedStatement.execute(sql, autoGeneratedKeys);
            }
    
    @Override
    public void setNCharacterStream(int parameterIndex, Reader value,
            long length) throws SQLException
            {
        realPreparedStatement
        .setNCharacterStream(parameterIndex, value, length);
            }
    
    @Override
    public void setNClob(int parameterIndex, NClob value) throws SQLException
    {
        realPreparedStatement.setNClob(parameterIndex, value);
    }
    
    @Override
    public void setClob(int parameterIndex, Reader reader, long length)
            throws SQLException
            {
        realPreparedStatement.setClob(parameterIndex, reader, length);
            }
    
    @Override
    public boolean execute(String sql, int[] columnIndexes) throws SQLException
    {
        return realPreparedStatement.execute(sql, columnIndexes);
    }

    public void setBlob(int parameterIndex, InputStream inputStream, long length)
            throws SQLException
            {
        realPreparedStatement.setBlob(parameterIndex, inputStream, length);
            }

    @Override
    public void setNClob(int parameterIndex, Reader reader, long length)
            throws SQLException
            {
        realPreparedStatement.setNClob(parameterIndex, reader, length);
            }

    @Override
    public boolean execute(String sql, String[] columnNames)
            throws SQLException
            {
        return realPreparedStatement.execute(sql, columnNames);
            }

    @Override
    public void setSQLXML(int parameterIndex, SQLXML xmlObject)
            throws SQLException
            {
        realPreparedStatement.setSQLXML(parameterIndex, xmlObject);
            }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType,
            int scaleOrLength) throws SQLException
            {
        realPreparedStatement.setObject(parameterIndex, x, targetSqlType,
                scaleOrLength);
            }

    @Override
    public int getResultSetHoldability() throws SQLException
    {
        return realPreparedStatement.getResultSetHoldability();
    }

    @Override
    public boolean isClosed() throws SQLException
    {
        return realPreparedStatement.isClosed();
    }

    @Override
    public void setPoolable(boolean poolable) throws SQLException
    {
        realPreparedStatement.setPoolable(poolable);
    }

    @Override
    public boolean isPoolable() throws SQLException
    {
        return realPreparedStatement.isPoolable();
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, long length)
            throws SQLException
            {
        realPreparedStatement.setAsciiStream(parameterIndex, x, length);
            }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, long length)
            throws SQLException
            {
        realPreparedStatement.setBinaryStream(parameterIndex, x, length);
            }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader,
            long length) throws SQLException
            {
        realPreparedStatement
        .setCharacterStream(parameterIndex, reader, length);
            }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x)
            throws SQLException
            {
        realPreparedStatement.setAsciiStream(parameterIndex, x);
            }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x)
            throws SQLException
            {
        realPreparedStatement.setBinaryStream(parameterIndex, x);
            }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader)
            throws SQLException
            {
        realPreparedStatement.setCharacterStream(parameterIndex, reader);
            }

    @Override
    public void setNCharacterStream(int parameterIndex, Reader value)
            throws SQLException
            {
        realPreparedStatement.setNCharacterStream(parameterIndex, value);
            }

    @Override
    public void setClob(int parameterIndex, Reader reader) throws SQLException
    {
        realPreparedStatement.setClob(parameterIndex, reader);
    }

    @Override
    public void setBlob(int parameterIndex, InputStream inputStream)
            throws SQLException
            {
        realPreparedStatement.setBlob(parameterIndex, inputStream);
            }

    @Override
    public void setNClob(int parameterIndex, Reader reader) throws SQLException
    {
        realPreparedStatement.setNClob(parameterIndex, reader);
    }    
}
